--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit 2b6cef04d02950e606e2a93d67c66b0c0ea31fab
Parents : d0db79e
Author : Sudo-Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-01-04T00:04:32-06:00
feat(App, ConfirmDialog, AboutPage, MapPage, ConversationViewer, IdentitiesPage): enhance UI components with improved styles, new features for note editing, and better user interactions
Changes
6 files changed, 1017 insertions(+), 733 deletions(-)
Diff
diff --git a/meshchatx/src/frontend/components/App.vue b/meshchatx/src/frontend/components/App.vue
index 1e4ecd6d..10ae5f59 100644
--- a/meshchatx/src/frontend/components/App.vue
+++ b/meshchatx/src/frontend/components/App.vue
@@ -24,9 +24,9 @@
<template v-else>
<!-- header -->
<div
- class="relative z-[60] flex bg-white/80 dark:bg-zinc-900/70 backdrop-blur border-gray-200 dark:border-zinc-800 border-b min-h-16 shadow-sm transition-colors"
+ class="relative z-[60] flex bg-white/60 dark:bg-zinc-900/50 backdrop-blur-lg border-gray-200 dark:border-zinc-800 border-b min-h-16 shadow-sm transition-colors overflow-x-hidden"
>
- <div class="flex w-full px-4">
+ <div class="flex w-full px-2 sm:px-4 overflow-x-auto no-scrollbar">
<button
type="button"
class="sm:hidden my-auto mr-4 text-gray-500 hover:text-gray-600 dark:text-gray-400 dark:hover:text-gray-300"
diff --git a/meshchatx/src/frontend/components/ConfirmDialog.vue b/meshchatx/src/frontend/components/ConfirmDialog.vue
index e8925a6a..aa273a8b 100644
--- a/meshchatx/src/frontend/components/ConfirmDialog.vue
+++ b/meshchatx/src/frontend/components/ConfirmDialog.vue
@@ -1,45 +1,38 @@
<template>
<Transition name="confirm-dialog">
- <div v-if="pendingConfirm" class="fixed inset-0 z-[200] flex items-center justify-center p-4">
- <div
- class="fixed inset-0 bg-black/50 backdrop-blur-sm sm:bg-transparent sm:backdrop-blur-none"
- @click="cancel"
- ></div>
+ <div v-if="pendingConfirm" class="fixed inset-0 z-[9999] flex items-center justify-center p-4">
+ <div class="fixed inset-0 bg-black/50 backdrop-blur-sm shadow-2xl" @click="cancel"></div>
<div
- class="relative w-full sm:w-auto sm:min-w-[360px] sm:max-w-md bg-white dark:bg-zinc-900 sm:rounded-lg rounded-2xl sm:shadow-lg shadow-2xl border border-gray-200 dark:border-zinc-800 overflow-hidden transform transition-all"
+ class="relative w-full sm:w-auto sm:min-w-[400px] sm:max-w-md bg-white dark:bg-zinc-900 sm:rounded-3xl rounded-3xl shadow-2xl border border-gray-200 dark:border-zinc-800 overflow-hidden transform transition-all"
@click.stop
>
- <div class="p-4 sm:p-4">
- <div class="flex items-start mb-4 sm:mb-3">
+ <div class="p-8">
+ <div class="flex items-start mb-6">
<div
- class="flex-shrink-0 flex items-center justify-center w-9 h-9 sm:w-7 sm:h-7 rounded-full bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400 mr-3 sm:mr-2.5"
+ class="flex-shrink-0 flex items-center justify-center w-12 h-12 rounded-2xl bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400 mr-4"
>
- <MaterialDesignIcon icon-name="alert-circle" class="w-5 h-5 sm:w-4 sm:h-4" />
+ <MaterialDesignIcon icon-name="alert-circle" class="w-6 h-6" />
</div>
<div class="flex-1 min-w-0">
- <h3 class="text-base sm:text-sm font-semibold text-gray-900 dark:text-white mb-1.5 sm:mb-1">
- Confirm
- </h3>
- <p
- class="text-sm sm:text-xs text-gray-600 dark:text-zinc-300 whitespace-pre-wrap leading-relaxed"
- >
+ <h3 class="text-xl font-black text-gray-900 dark:text-white mb-2">Confirm Action</h3>
+ <p class="text-gray-600 dark:text-zinc-300 whitespace-pre-wrap leading-relaxed">
{{ pendingConfirm.message }}
</p>
</div>
</div>
- <div class="flex flex-col-reverse sm:flex-row gap-2 sm:gap-2 sm:justify-end mt-5 sm:mt-3">
+ <div class="flex flex-col sm:flex-row gap-3 sm:justify-end mt-8">
<button
type="button"
- class="px-4 py-2 sm:px-3 sm:py-1.5 text-sm sm:text-xs font-medium text-gray-700 dark:text-zinc-300 bg-white dark:bg-zinc-800 border border-gray-300 dark:border-zinc-700 rounded-lg hover:bg-gray-50 dark:hover:bg-zinc-700 transition-colors"
+ class="px-6 py-3 text-sm font-bold text-gray-700 dark:text-zinc-300 bg-gray-100 dark:bg-zinc-800 rounded-xl hover:bg-gray-200 dark:hover:bg-zinc-700 transition-all active:scale-95"
@click="cancel"
>
Cancel
</button>
<button
type="button"
- class="px-4 py-2 sm:px-3 sm:py-1.5 text-sm sm:text-xs font-medium text-white bg-red-600 hover:bg-red-700 rounded-lg transition-colors"
+ class="px-6 py-3 text-sm font-bold text-white bg-red-600 hover:bg-red-700 rounded-xl shadow-lg shadow-red-600/20 transition-all active:scale-95"
@click="confirm"
>
Confirm
diff --git a/meshchatx/src/frontend/components/about/AboutPage.vue b/meshchatx/src/frontend/components/about/AboutPage.vue
index 16846f81..f608f107 100644
--- a/meshchatx/src/frontend/components/about/AboutPage.vue
+++ b/meshchatx/src/frontend/components/about/AboutPage.vue
@@ -2,746 +2,688 @@
<div
class="flex flex-col flex-1 overflow-hidden min-w-0 bg-gradient-to-br from-slate-50 via-slate-100 to-white dark:from-zinc-950 dark:via-zinc-900 dark:to-zinc-900"
>
- <div class="flex-1 overflow-y-auto w-full px-4 md:px-8 py-6">
- <div class="space-y-4 w-full max-w-6xl mx-auto">
- <div v-if="appInfo" class="glass-card">
- <div class="flex flex-col gap-4 md:flex-row md:items-center">
- <div class="flex-1 space-y-2">
- <div class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">
- {{ $t("about.title") }}
+ <div class="flex-1 overflow-y-auto w-full px-4 md:px-8 py-6 text-gray-900 dark:text-zinc-100">
+ <div class="space-y-6 w-full max-w-4xl mx-auto pb-20">
+ <!-- Basic Info Card -->
+ <div v-if="appInfo" class="glass-card !p-8">
+ <div class="flex flex-col gap-8 md:flex-row md:items-center">
+ <!-- Logo & Title -->
+ <div class="flex items-center gap-6">
+ <div
+ class="w-20 h-20 rounded-3xl bg-blue-500/10 flex items-center justify-center border border-blue-500/20 shadow-xl"
+ >
+ <img src="../../public/favicons/favicon-512x512.png" class="w-12 h-12 object-contain" />
</div>
- <div class="text-3xl font-semibold text-gray-900 dark:text-white">Reticulum MeshChatX</div>
- <div class="text-sm text-gray-600 dark:text-gray-300">
- {{ $t("about.version", { version: appInfo.version }) }} •
- {{ $t("about.rns_version", { version: appInfo.rns_version }) }} •
- {{ $t("about.lxmf_version", { version: appInfo.lxmf_version }) }} •
- {{ $t("about.python_version", { version: appInfo.python_version }) }}
+ <div class="space-y-1">
+ <div
+ class="text-4xl font-black text-gray-900 dark:text-white leading-none tracking-tight"
+ >
+ MeshChatX
+ </div>
+ <div class="text-sm font-black text-blue-500 uppercase tracking-[0.2em] opacity-80">
+ {{ $t("about.version", { version: appInfo.version }) }}
+ </div>
</div>
</div>
- <div class="flex flex-col sm:flex-row gap-2">
+
+ <div class="flex-1 md:text-right flex flex-wrap justify-start md:justify-end gap-3">
<button
type="button"
- class="danger-chip px-4 py-2 text-sm justify-center"
- @click="shutdown"
+ class="secondary-chip px-5 py-3 text-sm font-black transition-all hover:scale-105 active:scale-95"
+ @click="showTutorial"
>
- <MaterialDesignIcon icon-name="power" class="w-4 h-4" />
- {{ $t("common.shutdown", "Shutdown") }}
+ <v-icon icon="mdi-help-circle" size="20" class="mr-2"></v-icon>
+ {{ $t("app.tutorial_title") }}
</button>
<button
type="button"
- class="secondary-chip px-4 py-2 text-sm justify-center"
+ class="secondary-chip px-5 py-3 text-sm font-black transition-all hover:scale-105 active:scale-95"
@click="showChangelog"
>
- <MaterialDesignIcon icon-name="history" class="w-4 h-4" />
+ <v-icon icon="mdi-history" size="20" class="mr-2"></v-icon>
{{ $t("app.changelog_title") }}
</button>
- <button
- type="button"
- class="secondary-chip px-4 py-2 text-sm justify-center"
- @click="showTutorial"
- >
- <MaterialDesignIcon icon-name="help-circle" class="w-4 h-4" />
- {{ $t("app.tutorial_title") }}
- </button>
<button
v-if="isElectron"
type="button"
- class="primary-chip px-4 py-2 text-sm justify-center"
+ class="primary-chip px-5 py-3 text-sm font-black transition-all hover:scale-105 active:scale-95"
@click="relaunch"
>
- <MaterialDesignIcon icon-name="restart" class="w-4 h-4" />
+ <v-icon icon="mdi-restart" size="20" class="mr-2"></v-icon>
{{ $t("common.restart_app") }}
</button>
- </div>
- </div>
- <div class="grid gap-3 sm:grid-cols-3 mt-4 text-sm text-gray-700 dark:text-gray-300">
- <div>
- <div class="glass-label">{{ $t("about.config_path") }}</div>
- <div class="monospace-field break-all">{{ appInfo.reticulum_config_path }}</div>
- <button
- v-if="isElectron"
- type="button"
- class="secondary-chip mt-2 text-xs"
- @click="showReticulumConfigFile"
- >
- <MaterialDesignIcon icon-name="folder" class="w-4 h-4" />
- {{ $t("common.reveal") }}
- </button>
- </div>
- <div>
- <div class="glass-label">{{ $t("about.database_path") }}</div>
- <div class="monospace-field break-all">{{ appInfo.database_path }}</div>
<button
- v-if="isElectron"
type="button"
- class="secondary-chip mt-2 text-xs"
- @click="showDatabaseFile"
+ class="danger-chip px-5 py-3 text-sm font-black transition-all hover:scale-105 active:scale-95"
+ @click="shutdown"
>
- <MaterialDesignIcon icon-name="database" class="w-4 h-4" />
- {{ $t("common.reveal") }}
+ <v-icon icon="mdi-power" size="20" class="mr-2"></v-icon>
+ {{ $t("common.shutdown", "Shutdown") }}
</button>
</div>
- <div>
- <div class="glass-label">{{ $t("about.database_size") }}</div>
- <div class="text-lg font-semibold text-gray-900 dark:text-white">
- {{
- formatBytes(
- appInfo.database_files
- ? appInfo.database_files.total_bytes
- : appInfo.database_file_size
- )
- }}
- </div>
- <div v-if="appInfo.database_files" class="text-xs text-gray-500 dark:text-gray-400">
- Main {{ formatBytes(appInfo.database_files.main_bytes) }} • WAL
- {{ formatBytes(appInfo.database_files.wal_bytes) }}
- </div>
- </div>
</div>
- </div>
- <div v-if="appInfo" class="glass-card">
- <div class="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
- <div>
- <div class="text-lg font-semibold text-gray-900 dark:text-white">
- {{ $t("about.security_integrity") }}
- </div>
- <div class="text-xs text-gray-600 dark:text-gray-400">
- {{ $t("about.security_integrity_description") }}
- </div>
- </div>
- <div v-if="appInfo.integrity_issues" class="flex flex-wrap gap-2">
- <span :class="statusPillClass(appInfo.integrity_issues.length === 0)">
- <MaterialDesignIcon
- :icon-name="appInfo.integrity_issues.length === 0 ? 'shield-check' : 'shield-alert'"
- class="w-4 h-4"
- />
- {{
- appInfo.integrity_issues.length === 0
- ? $t("about.secured")
- : $t("about.tampering_detected")
- }}
- </span>
- <button
- v-if="appInfo.integrity_issues.length > 0"
- type="button"
- class="secondary-chip px-3 py-1 text-xs"
- @click="acknowledgeIntegrity"
- >
- <MaterialDesignIcon icon-name="check-circle-outline" class="w-4 h-4" />
- {{ $t("common.acknowledge_reset") }}
- </button>
- </div>
- </div>
<div
- v-if="appInfo.integrity_issues && appInfo.integrity_issues.length > 0"
- class="mt-4 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg"
+ class="mt-10 pt-8 border-t border-gray-100 dark:border-zinc-800 flex flex-col md:flex-row md:items-center justify-between gap-6"
>
- <div class="text-sm font-semibold text-red-700 dark:text-red-400 mb-2">
- {{ $t("about.technical_issues") }}
+ <div class="text-gray-600 dark:text-zinc-400 max-w-xl text-lg leading-relaxed">
+ A secure, resilient, and beautiful communications platform powered by the
+ <a
+ href="https://reticulum.network"
+ target="_blank"
+ class="text-blue-500 font-black hover:underline decoration-2 underline-offset-4"
+ >Reticulum Network Stack</a
+ >.
</div>
- <ul class="text-xs text-red-600 dark:text-red-300 space-y-1 list-disc list-inside">
- <li v-for="(issue, index) in appInfo.integrity_issues" :key="index">{{ issue }}</li>
- </ul>
- </div>
- <div v-else class="mt-4 text-sm text-gray-500 dark:text-gray-400 flex items-center gap-2">
- <MaterialDesignIcon icon-name="check-circle" class="w-4 h-4 text-emerald-500" />
- {{ $t("about.no_integrity_violations") }}
- </div>
- </div>
-
- <div v-if="appInfo" class="glass-card">
- <div class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
- {{ $t("about.dependency_chain") }}
- </div>
- <div class="grid grid-cols-1 lg:grid-cols-2 gap-8 relative">
- <!-- Main Dependencies -->
- <div class="flex flex-col space-y-4">
- <!-- MeshChatX -->
- <div class="flex items-center gap-4">
+ <div class="flex items-center gap-6 shrink-0">
+ <div class="text-right">
<div
- class="w-10 h-10 rounded-full bg-blue-500/10 flex items-center justify-center border border-blue-500/20"
+ class="text-[10px] font-black text-gray-400 dark:text-zinc-500 uppercase tracking-[0.2em] leading-none mb-1"
>
- <img
- src="../../public/favicons/favicon-512x512.png"
- class="w-6 h-6 object-contain"
- />
+ Database Size
</div>
- <div class="flex-1">
- <div class="text-sm font-bold text-gray-900 dark:text-white">MeshChatX</div>
- <div class="text-xs text-gray-500 dark:text-gray-400">v{{ appInfo.version }}</div>
+ <div class="text-2xl font-black text-gray-900 dark:text-white tabular-nums">
+ {{
+ formatBytes(
+ appInfo.database_files
+ ? appInfo.database_files.total_bytes
+ : appInfo.database_file_size
+ )
+ }}
</div>
</div>
+ <div class="w-px h-12 bg-gray-100 dark:bg-zinc-800"></div>
+ <v-btn
+ variant="tonal"
+ color="blue"
+ height="48"
+ class="font-black px-6 rounded-xl"
+ @click="showAdvanced = !showAdvanced"
+ >
+ <v-icon :icon="showAdvanced ? 'mdi-chevron-up' : 'mdi-xml'" start></v-icon>
+ {{ showAdvanced ? "Hide Advanced" : "Advanced Mode" }}
+ </v-btn>
+ </div>
+ </div>
+ </div>
- <!-- Connector -->
- <div class="ml-5 border-l-2 border-gray-200 dark:border-zinc-800 h-4"></div>
-
- <!-- LXMF -->
- <div class="flex items-center gap-4">
- <div
- class="w-10 h-10 rounded-full bg-purple-500/10 flex items-center justify-center border border-purple-500/20 text-purple-600 font-black text-xs"
- >
- LXMF
- </div>
- <div class="flex-1">
- <div class="text-sm font-bold text-gray-900 dark:text-white">
- Lightweight Extensible Message Format
- </div>
- <div class="text-xs text-gray-500 dark:text-gray-400">
- v{{ appInfo.lxmf_version }}
- </div>
- </div>
+ <transition name="fade-blur">
+ <div v-if="showAdvanced" class="space-y-6">
+ <!-- Advanced Tech Info -->
+ <div v-if="appInfo" class="glass-card !p-6">
+ <div
+ class="text-xs font-black text-blue-500 uppercase tracking-[0.2em] mb-6 flex items-center gap-2"
+ >
+ <v-icon icon="mdi-server" size="14"></v-icon>
+ Environment Information
</div>
-
- <!-- Connector -->
- <div class="ml-5 border-l-2 border-gray-200 dark:border-zinc-800 h-4"></div>
-
- <!-- RNS -->
- <div class="flex items-center gap-4">
- <div
- class="w-10 h-10 rounded-full bg-indigo-500/10 flex items-center justify-center border border-indigo-500/20 text-indigo-600 font-black text-xs"
- >
- RNS
- </div>
- <div class="flex-1">
- <div class="text-sm font-bold text-gray-900 dark:text-white">
- Reticulum Network Stack
+ <div class="grid gap-8 sm:grid-cols-2 lg:grid-cols-3 text-sm">
+ <div>
+ <div class="glass-label !text-[10px] mb-2 opacity-50">Reticulum Config</div>
+ <div
+ class="monospace-field !bg-zinc-50 dark:!bg-zinc-950 break-all text-[11px] !p-3 rounded-xl border border-zinc-100 dark:border-zinc-800"
+ >
+ {{ appInfo.reticulum_config_path }}
</div>
- <div class="text-xs text-gray-500 dark:text-gray-400">
- v{{ appInfo.rns_version }}
+ <v-btn
+ v-if="isElectron"
+ variant="tonal"
+ size="x-small"
+ class="mt-3 font-black px-3"
+ @click="showReticulumConfigFile"
+ >
+ <v-icon icon="mdi-folder-open" start size="14"></v-icon> Reveal File
+ </v-btn>
+ </div>
+ <div>
+ <div class="glass-label !text-[10px] mb-2 opacity-50">Database Path</div>
+ <div
+ class="monospace-field !bg-zinc-50 dark:!bg-zinc-950 break-all text-[11px] !p-3 rounded-xl border border-zinc-100 dark:border-zinc-800"
+ >
+ {{ appInfo.database_path }}
</div>
+ <v-btn
+ v-if="isElectron"
+ variant="tonal"
+ size="x-small"
+ class="mt-3 font-black px-3"
+ @click="showDatabaseFile"
+ >
+ <v-icon icon="mdi-database-search" start size="14"></v-icon> Reveal DB
+ </v-btn>
</div>
- </div>
- </div>
-
- <!-- Side & Python Dependencies -->
- <div class="space-y-8">
- <!-- Side Dependencies -->
- <div>
<div
- class="text-xs font-bold text-gray-400 dark:text-zinc-500 uppercase tracking-wider mb-3"
+ class="flex flex-col justify-center space-y-3 bg-zinc-50 dark:bg-zinc-950 p-4 rounded-xl border border-zinc-100 dark:border-zinc-800"
>
- {{ $t("about.other_core_components", "Other Core Components") }}
- </div>
- <div class="grid grid-cols-2 sm:grid-cols-3 gap-4">
- <div v-if="appInfo.lxst_version" class="flex flex-col">
- <span class="text-[10px] font-black text-blue-500 uppercase">LXST</span>
- <span class="text-sm font-medium text-gray-700 dark:text-zinc-300"
- >v{{ appInfo.lxst_version }}</span
+ <div class="flex items-center justify-between">
+ <span class="text-[10px] font-black text-blue-500 uppercase tracking-wider"
+ >Python</span
>
+ <span class="font-mono text-xs font-bold">v{{ appInfo.python_version }}</span>
</div>
- <div class="flex flex-col">
- <span class="text-[10px] font-black text-blue-500 uppercase">Python</span>
- <span class="text-sm font-medium text-gray-700 dark:text-zinc-300"
- >v{{ appInfo.python_version }}</span
+ <div class="flex items-center justify-between">
+ <span class="text-[10px] font-black text-purple-500 uppercase tracking-wider"
+ >LXMF</span
>
+ <span class="font-mono text-xs font-bold">v{{ appInfo.lxmf_version }}</span>
</div>
- <div v-if="electronVersion" class="flex flex-col">
- <span class="text-[10px] font-black text-blue-500 uppercase">Electron</span>
- <span class="text-sm font-medium text-gray-700 dark:text-zinc-300"
- >v{{ electronVersion }}</span
- >
- </div>
- <div v-if="chromeVersion" class="flex flex-col">
- <span class="text-[10px] font-black text-blue-500 uppercase">Chrome</span>
- <span class="text-sm font-medium text-gray-700 dark:text-zinc-300"
- >v{{ chromeVersion }}</span
- >
- </div>
- <div v-if="nodeVersion" class="flex flex-col">
- <span class="text-[10px] font-black text-blue-500 uppercase">Node</span>
- <span class="text-sm font-medium text-gray-700 dark:text-zinc-300"
- >v{{ nodeVersion }}</span
+ <div class="flex items-center justify-between">
+ <span class="text-[10px] font-black text-indigo-500 uppercase tracking-wider"
+ >RNS</span
>
+ <span class="font-mono text-xs font-bold">v{{ appInfo.rns_version }}</span>
</div>
</div>
</div>
+ </div>
- <!-- Python Dependencies -->
- <div v-if="appInfo.dependencies" class="pt-6 border-t border-gray-100 dark:border-zinc-800">
+ <!-- Security & Integrity -->
+ <div v-if="appInfo" class="glass-card !p-6">
+ <div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between mb-6">
<div
- class="text-xs font-bold text-gray-400 dark:text-zinc-500 uppercase tracking-wider mb-3"
+ class="text-xs font-black text-blue-500 uppercase tracking-[0.2em] flex items-center gap-2"
>
- {{ $t("about.backend_dependencies") }}
+ <v-icon icon="mdi-shield-lock" size="14"></v-icon>
+ Security & Integrity
</div>
- <div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
- <div
- v-for="(version, name) in appInfo.dependencies"
- :key="name"
- class="flex flex-col"
+ <div v-if="appInfo.integrity_issues" class="flex flex-wrap gap-2">
+ <span
+ :class="statusPillClass(appInfo.integrity_issues.length === 0)"
+ class="font-black px-3 py-1 text-[11px]"
>
- <span class="text-[10px] font-black text-blue-500/70 uppercase">{{
- name.replace("_", " ")
- }}</span>
- <span class="text-xs font-medium text-gray-600 dark:text-zinc-400"
- >v{{ version }}</span
- >
- </div>
+ <v-icon
+ :icon="
+ appInfo.integrity_issues.length === 0
+ ? 'mdi-shield-check'
+ : 'mdi-shield-alert'
+ "
+ size="14"
+ start
+ ></v-icon>
+ {{
+ appInfo.integrity_issues.length === 0
+ ? $t("about.secured")
+ : $t("about.tampering_detected")
+ }}
+ </span>
+ <button
+ v-if="appInfo.integrity_issues.length > 0"
+ type="button"
+ class="secondary-chip px-3 py-1 text-[11px] font-black"
+ @click="acknowledgeIntegrity"
+ >
+ <v-icon icon="mdi-check-circle" size="14" start></v-icon>
+ {{ $t("common.acknowledge_reset") }}
+ </button>
</div>
</div>
- </div>
- </div>
- </div>
- <div class="glass-card space-y-4">
- <div class="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
- <div>
- <div class="text-lg font-semibold text-gray-900 dark:text-white">
- {{ $t("about.database_health") }}
- </div>
- <div class="text-xs text-gray-600 dark:text-gray-400">
- {{ $t("about.database_health_description") }}
- </div>
- </div>
- <div class="flex flex-wrap gap-2">
- <button
- type="button"
- class="secondary-chip px-3 py-2 text-xs"
- :disabled="databaseActionInProgress || healthLoading"
- @click="getDatabaseHealth(true)"
- >
- <MaterialDesignIcon icon-name="refresh" class="w-4 h-4" />
- {{ $t("common.refresh") }}
- </button>
- <button
- type="button"
- class="secondary-chip px-3 py-2 text-xs"
- :disabled="databaseActionInProgress || healthLoading"
- @click="vacuumDatabase"
- >
- <MaterialDesignIcon icon-name="broom" class="w-4 h-4" />
- {{ $t("common.vacuum") }}
- </button>
- <button
- type="button"
- class="primary-chip px-3 py-2 text-xs"
- :disabled="databaseActionInProgress || healthLoading"
- @click="recoverDatabase"
+ <div
+ v-if="appInfo.integrity_issues && appInfo.integrity_issues.length > 0"
+ class="p-4 bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-900 rounded-xl"
>
- <MaterialDesignIcon icon-name="shield-sync" class="w-4 h-4" />
- {{ $t("common.auto_recover") }}
- </button>
- </div>
- </div>
- <div v-if="databaseActionMessage" class="text-xs text-emerald-600">{{ databaseActionMessage }}</div>
- <div v-if="databaseActionError" class="text-xs text-red-600">{{ databaseActionError }}</div>
- <div v-if="healthLoading" class="text-sm text-gray-500 dark:text-gray-400">
- {{ $t("about.running_checks") }}
- </div>
- <div
- v-if="databaseHealth"
- class="grid gap-3 sm:grid-cols-3 text-sm text-gray-700 dark:text-gray-300"
- >
- <div>
- <div class="glass-label">{{ $t("about.integrity") }}</div>
- <div class="metric-value">{{ databaseHealth.quick_check }}</div>
- </div>
- <div>
- <div class="glass-label">{{ $t("about.journal_mode") }}</div>
- <div class="metric-value">{{ databaseHealth.journal_mode }}</div>
- </div>
- <div>
- <div class="glass-label">{{ $t("about.wal_autocheckpoint") }}</div>
- <div class="metric-value">
- {{
- databaseHealth.wal_autocheckpoint !== null &&
- databaseHealth.wal_autocheckpoint !== undefined
- ? databaseHealth.wal_autocheckpoint
- : "—"
- }}
- </div>
- </div>
- <div>
- <div class="glass-label">{{ $t("about.page_size") }}</div>
- <div class="metric-value">{{ formatBytes(databaseHealth.page_size) }}</div>
- </div>
- <div>
- <div class="glass-label">{{ $t("about.pages_free") }}</div>
- <div class="metric-value">
- {{ formatNumber(databaseHealth.page_count) }} /
- {{ formatNumber(databaseHealth.freelist_pages) }}
+ <div
+ class="text-xs font-black text-red-700 dark:text-red-400 mb-3 uppercase tracking-wider flex items-center gap-2"
+ >
+ <v-icon icon="mdi-alert-octagon" size="16"></v-icon>
+ Technical Issues Detected
+ </div>
+ <ul class="text-[11px] text-red-600 dark:text-red-300 space-y-2 list-none font-mono">
+ <li
+ v-for="(issue, index) in appInfo.integrity_issues"
+ :key="index"
+ class="flex gap-2"
+ >
+ <span class="opacity-50">•</span>
+ <span>{{ issue }}</span>
+ </li>
+ </ul>
</div>
- </div>
- <div>
- <div class="glass-label">{{ $t("about.free_space_estimate") }}</div>
- <div class="metric-value">{{ formatBytes(databaseHealth.estimated_free_bytes) }}</div>
- </div>
- </div>
- <div v-else-if="!healthLoading" class="text-sm text-gray-500 dark:text-gray-400">
- Health data will appear after the first refresh.
- </div>
- <div
- v-if="databaseRecoveryActions.length"
- class="text-xs text-gray-600 dark:text-gray-400 border-t border-gray-200 dark:border-gray-800 pt-3"
- >
- <div class="font-semibold text-gray-800 dark:text-gray-200 mb-1">Last recovery steps</div>
- <ul class="list-disc list-inside space-y-1">
- <li v-for="(action, index) in databaseRecoveryActions" :key="index">
- <span class="font-medium text-gray-700 dark:text-gray-300">{{ action.step }}:</span>
- <span class="ml-1">{{ formatRecoveryResult(action.result) }}</span>
- </li>
- </ul>
- </div>
- <div class="border-t border-gray-200 dark:border-gray-800 pt-3 space-y-3">
- <div class="font-semibold text-gray-900 dark:text-white">Backups</div>
- <button
- type="button"
- class="secondary-chip px-3 py-2 text-xs"
- :disabled="backupInProgress"
- @click="backupDatabase"
- >
- <MaterialDesignIcon icon-name="content-save" class="w-4 h-4" />
- Download Backup
- </button>
- <div v-if="backupMessage" class="text-xs text-emerald-600">{{ backupMessage }}</div>
- <div v-if="backupError" class="text-xs text-red-600">{{ backupError }}</div>
- <div class="font-semibold text-gray-900 dark:text-white pt-2">Snapshots</div>
- <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3">
- <input
- v-model="snapshotName"
- type="text"
- placeholder="Snapshot name"
- class="input-field max-w-xs"
- />
- <button
- type="button"
- class="primary-chip px-3 py-2 text-xs"
- :disabled="snapshotInProgress"
- @click="createSnapshot"
- >
- <MaterialDesignIcon icon-name="camera" class="w-4 h-4" />
- Create Snapshot
- </button>
- </div>
- <div v-if="snapshots.length > 0" class="mt-3 space-y-2">
<div
- v-for="snapshot in snapshots"
- :key="snapshot.path"
- class="flex items-center justify-between p-2 rounded-lg bg-gray-50 dark:bg-zinc-800 border border-gray-100 dark:border-zinc-700 text-xs"
+ v-else
+ class="text-sm text-gray-500 dark:text-zinc-500 flex items-center gap-3 bg-emerald-500/5 p-4 rounded-xl border border-emerald-500/10"
>
- <div class="flex flex-col">
- <span class="font-bold text-gray-900 dark:text-white">{{ snapshot.name }}</span>
- <span class="text-gray-500"
- >{{ formatBytes(snapshot.size) }} • {{ snapshot.created_at }}</span
- >
- </div>
- <button
- type="button"
- class="secondary-chip px-2 py-1"
- @click="restoreFromSnapshot(snapshot.path)"
- >
- Restore
- </button>
+ <v-icon icon="mdi-check-decagram" color="emerald" size="20"></v-icon>
+ <span class="font-bold tracking-tight">{{ $t("about.no_integrity_violations") }}</span>
</div>
</div>
- <div v-if="snapshotMessage" class="text-xs text-emerald-600">{{ snapshotMessage }}</div>
- <div v-if="snapshotError" class="text-xs text-red-600">{{ snapshotError }}</div>
- <div class="font-semibold text-gray-900 dark:text-white pt-2">Restore</div>
- <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3">
- <input type="file" accept=".zip,.db" class="file-input" @change="onRestoreFileChange" />
- <button
- type="button"
- class="primary-chip px-3 py-2 text-xs"
- :disabled="restoreInProgress"
- @click="restoreDatabase"
+ <!-- Dependency Chain -->
+ <div v-if="appInfo" class="glass-card !p-6">
+ <div
+ class="text-xs font-black text-blue-500 uppercase tracking-[0.2em] mb-8 flex items-center gap-2"
>
- <MaterialDesignIcon icon-name="database-sync" class="w-4 h-4" />
- Restore
- </button>
- </div>
- <div v-if="restoreFileName" class="text-xs text-gray-600 dark:text-gray-400">
- Selected: {{ restoreFileName }}
- </div>
- <div v-if="restoreMessage" class="text-xs text-emerald-600">{{ restoreMessage }}</div>
- <div v-if="restoreError" class="text-xs text-red-600">{{ restoreError }}</div>
- <div class="border-t border-gray-200 dark:border-gray-800 pt-3 space-y-3">
- <div class="font-semibold text-gray-900 dark:text-white">Identity Backup & Restore</div>
- <div class="text-xs text-red-600">
- Never share this identity. It grants full control. Clear your clipboard after copying.
- </div>
- <div class="flex flex-wrap gap-2">
- <button
- type="button"
- class="secondary-chip px-3 py-2 text-xs"
- @click="downloadIdentityFile"
- >
- <MaterialDesignIcon icon-name="content-save" class="w-4 h-4" />
- Download Identity File
- </button>
- <button
- type="button"
- class="secondary-chip px-3 py-2 text-xs"
- @click="copyIdentityBase32"
- >
- <MaterialDesignIcon icon-name="content-copy" class="w-4 h-4" />
- Copy Base32 Identity
- </button>
- </div>
- <div v-if="identityBackupMessage" class="text-xs text-emerald-600">
- {{ identityBackupMessage }}
+ <v-icon icon="mdi-link-variant" size="14"></v-icon>
+ Dependency Chain
</div>
- <div v-if="identityBackupError" class="text-xs text-red-600">{{ identityBackupError }}</div>
- <div v-if="identityBase32Message" class="text-xs text-emerald-600">
- {{ identityBase32Message }}
- </div>
- <div v-if="identityBase32Error" class="text-xs text-red-600">{{ identityBase32Error }}</div>
- <div class="font-semibold text-gray-900 dark:text-white pt-2">Restore from file</div>
- <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3">
- <input
- type="file"
- accept=".identity,.bin,.key"
- class="file-input"
- @change="onIdentityRestoreFileChange"
- />
- <button
- type="button"
- class="primary-chip px-3 py-2 text-xs"
- :disabled="identityRestoreInProgress"
- @click="restoreIdentityFile"
- >
- <MaterialDesignIcon icon-name="database-sync" class="w-4 h-4" />
- Restore Identity
- </button>
- </div>
- <div v-if="identityRestoreFileName" class="text-xs text-gray-600 dark:text-gray-400">
- Selected: {{ identityRestoreFileName }}
- </div>
- <div class="font-semibold text-gray-900 dark:text-white pt-2">Restore from base32</div>
- <textarea v-model="identityRestoreBase32" rows="3" class="input-field"></textarea>
- <button
- type="button"
- class="primary-chip px-3 py-2 text-xs"
- :disabled="identityRestoreInProgress"
- @click="restoreIdentityBase32"
- >
- <MaterialDesignIcon icon-name="database-sync" class="w-4 h-4" />
- Restore Identity
- </button>
- <div v-if="identityRestoreMessage" class="text-xs text-emerald-600">
- {{ identityRestoreMessage }}
- </div>
- <div v-if="identityRestoreError" class="text-xs text-red-600">
- {{ identityRestoreError }}
- </div>
- </div>
- </div>
- </div>
-
- <div class="grid gap-4 lg:grid-cols-2">
- <div v-if="appInfo?.memory_usage" class="glass-card space-y-3">
- <header class="flex items-center gap-2">
- <MaterialDesignIcon icon-name="chip" class="w-5 h-5 text-blue-500" />
- <div>
- <div class="text-lg font-semibold text-gray-900 dark:text-white">
- {{ $t("about.system_resources") }}
- </div>
- <div class="text-xs text-emerald-500 flex items-center gap-1">
- <span class="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span>
- {{ $t("about.live") }}
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-12 relative">
+ <div class="flex flex-col space-y-8">
+ <div class="flex items-center gap-5">
+ <div
+ class="w-12 h-12 rounded-2xl bg-blue-500/10 flex items-center justify-center border border-blue-500/20 shadow-sm"
+ >
+ <img
+ src="../../public/favicons/favicon-512x512.png"
+ class="w-7 h-7 object-contain"
+ />
+ </div>
+ <div>
+ <div class="text-sm font-black text-gray-900 dark:text-white">
+ MeshChatX
+ </div>
+ <div class="text-xs font-mono font-bold text-gray-400">
+ v{{ appInfo.version }}
+ </div>
+ </div>
+ </div>
+ <div
+ class="flex items-center gap-5 pl-5 border-l-2 border-zinc-100 dark:border-zinc-800 ml-6 relative"
+ >
+ <div
+ class="absolute -left-[2px] top-0 bottom-0 w-[2px] bg-gradient-to-b from-blue-500 to-purple-500"
+ ></div>
+ <div
+ class="w-12 h-12 rounded-2xl bg-purple-500/10 flex items-center justify-center border border-purple-500/20 text-purple-600 font-black text-[10px] tracking-tighter shadow-sm"
+ >
+ LXMF
+ </div>
+ <div>
+ <div class="text-sm font-black text-gray-900 dark:text-white leading-tight">
+ Lightweight Extensible Message Format
+ </div>
+ <div class="text-xs font-mono font-bold text-gray-400 mt-1">
+ v{{ appInfo.lxmf_version }}
+ </div>
+ </div>
+ </div>
+ <div
+ class="flex items-center gap-5 pl-5 border-l-2 border-zinc-100 dark:border-zinc-800 ml-6 relative"
+ >
+ <div
+ class="absolute -left-[2px] top-0 bottom-0 w-[2px] bg-gradient-to-b from-purple-500 to-indigo-500"
+ ></div>
+ <div
+ class="w-12 h-12 rounded-2xl bg-indigo-500/10 flex items-center justify-center border border-indigo-500/20 text-indigo-600 font-black text-[10px] tracking-tighter shadow-sm"
+ >
+ RNS
+ </div>
+ <div>
+ <div class="text-sm font-black text-gray-900 dark:text-white leading-tight">
+ Reticulum Network Stack
+ </div>
+ <div class="text-xs font-mono font-bold text-gray-400 mt-1">
+ v{{ appInfo.rns_version }}
+ </div>
+ </div>
+ </div>
</div>
- </div>
- </header>
- <div class="metric-row">
- <div>
- <div class="glass-label">{{ $t("about.memory_rss") }}</div>
- <div class="metric-value">{{ formatBytes(appInfo.memory_usage.rss) }}</div>
- </div>
- <div>
- <div class="glass-label">{{ $t("about.virtual_memory") }}</div>
- <div class="metric-value">{{ formatBytes(appInfo.memory_usage.vms) }}</div>
- </div>
- </div>
- </div>
- <div v-if="electronMemoryUsage" class="glass-card space-y-3">
- <header class="flex items-center gap-2">
- <MaterialDesignIcon icon-name="electron-framework" class="w-5 h-5 text-blue-400" />
- <div>
- <div class="text-lg font-semibold text-gray-900 dark:text-white">
- Electron Resources
- </div>
- <div class="text-xs text-emerald-500 flex items-center gap-1">
- <span class="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span>
- {{ $t("about.live") }}
- </div>
- </div>
- </header>
- <div class="metric-row">
- <div>
- <div class="glass-label">Private Memory</div>
- <div class="metric-value">{{ formatBytes(electronMemoryUsage.private * 1024) }}</div>
- </div>
- <div>
- <div class="glass-label">Resident Set</div>
- <div class="metric-value">
- {{ formatBytes(electronMemoryUsage.residentSet * 1024) }}
+ <div class="space-y-8">
+ <div
+ class="bg-zinc-50 dark:bg-zinc-950 p-5 rounded-2xl border border-zinc-100 dark:border-zinc-800"
+ >
+ <div
+ class="text-[10px] font-black text-gray-400 dark:text-zinc-600 uppercase tracking-[0.2em] mb-4"
+ >
+ Core Runtime
+ </div>
+ <div class="grid grid-cols-2 gap-x-6 gap-y-4">
+ <div v-if="appInfo.lxst_version" class="flex flex-col">
+ <span
+ class="text-[9px] font-black text-blue-500/60 uppercase leading-none"
+ >LXST Engine</span
+ >
+ <span
+ class="text-[11px] font-mono font-bold mt-1.5 opacity-90 tracking-tight"
+ >v{{ appInfo.lxst_version }}</span
+ >
+ </div>
+ <div v-if="electronVersion" class="flex flex-col">
+ <span
+ class="text-[9px] font-black text-blue-500/60 uppercase leading-none"
+ >Electron</span
+ >
+ <span
+ class="text-[11px] font-mono font-bold mt-1.5 opacity-90 tracking-tight"
+ >v{{ electronVersion }}</span
+ >
+ </div>
+ <div v-if="chromeVersion" class="flex flex-col">
+ <span
+ class="text-[9px] font-black text-blue-500/60 uppercase leading-none"
+ >Chrome</span
+ >
+ <span
+ class="text-[11px] font-mono font-bold mt-1.5 opacity-90 tracking-tight"
+ >v{{ chromeVersion }}</span
+ >
+ </div>
+ <div v-if="nodeVersion" class="flex flex-col">
+ <span
+ class="text-[9px] font-black text-blue-500/60 uppercase leading-none"
+ >Node.js</span
+ >
+ <span
+ class="text-[11px] font-mono font-bold mt-1.5 opacity-90 tracking-tight"
+ >v{{ nodeVersion }}</span
+ >
+ </div>
+ </div>
+ </div>
+
+ <div v-if="appInfo.dependencies" class="pt-2">
+ <div
+ class="text-[10px] font-black text-gray-400 dark:text-zinc-600 uppercase tracking-[0.2em] mb-4"
+ >
+ Backend Stack
+ </div>
+ <div class="grid grid-cols-2 sm:grid-cols-3 gap-x-6 gap-y-4">
+ <div
+ v-for="(version, name) in appInfo.dependencies"
+ :key="name"
+ class="flex flex-col"
+ >
+ <span
+ class="text-[9px] font-black text-gray-400 dark:text-zinc-600 uppercase truncate leading-none"
+ >{{ name.replace("_", " ") }}</span
+ >
+ <span
+ class="text-[10px] font-mono font-bold mt-1.5 opacity-50 tracking-tight"
+ >v{{ version }}</span
+ >
+ </div>
+ </div>
+ </div>
</div>
</div>
</div>
- </div>
- <div v-if="appInfo?.network_stats" class="glass-card space-y-3">
- <header class="flex items-center gap-2">
- <MaterialDesignIcon icon-name="access-point-network" class="w-5 h-5 text-purple-500" />
- <div>
- <div class="text-lg font-semibold text-gray-900 dark:text-white">
- {{ $t("about.network_stats") }}
+ <!-- Database Health -->
+ <div class="glass-card !p-6">
+ <div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between mb-8">
+ <div
+ class="text-xs font-black text-blue-500 uppercase tracking-[0.2em] flex items-center gap-2"
+ >
+ <v-icon icon="mdi-database-cog" size="14"></v-icon>
+ Database Health & Maintenance
</div>
- <div class="text-xs text-emerald-500 flex items-center gap-1">
- <span class="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span>
- {{ $t("about.live") }}
+ <div class="flex flex-wrap gap-2">
+ <v-btn
+ variant="tonal"
+ size="small"
+ class="font-black px-4 rounded-lg"
+ :loading="healthLoading"
+ :disabled="databaseActionInProgress"
+ @click="getDatabaseHealth(true)"
+ >
+ <v-icon icon="mdi-refresh" start size="14"></v-icon> Refresh
+ </v-btn>
+ <v-btn
+ variant="flat"
+ color="blue"
+ size="small"
+ class="font-black px-4 rounded-lg"
+ :loading="databaseActionInProgress"
+ @click="vacuumDatabase"
+ >
+ <v-icon icon="mdi-broom" start size="14"></v-icon> Vacuum
+ </v-btn>
+ <v-btn
+ variant="flat"
+ color="amber"
+ size="small"
+ class="font-black px-4 rounded-lg text-white"
+ :loading="databaseActionInProgress"
+ @click="recoverDatabase"
+ >
+ <v-icon icon="mdi-medical-bag" start size="14"></v-icon> Recovery
+ </v-btn>
</div>
</div>
- </header>
- <div class="metric-row">
- <div>
- <div class="glass-label">{{ $t("about.sent") }}</div>
- <div class="metric-value">{{ formatBytes(appInfo.network_stats.bytes_sent) }}</div>
- </div>
- <div>
- <div class="glass-label">{{ $t("about.received") }}</div>
- <div class="metric-value">{{ formatBytes(appInfo.network_stats.bytes_recv) }}</div>
- </div>
- </div>
- <div class="metric-row">
- <div>
- <div class="glass-label">{{ $t("about.packets_sent") }}</div>
- <div class="metric-value">{{ formatNumber(appInfo.network_stats.packets_sent) }}</div>
- </div>
- <div>
- <div class="glass-label">{{ $t("about.packets_received") }}</div>
- <div class="metric-value">{{ formatNumber(appInfo.network_stats.packets_recv) }}</div>
- </div>
- </div>
- </div>
- <div v-if="appInfo?.reticulum_stats" class="glass-card space-y-3">
- <header class="flex items-center gap-2">
- <MaterialDesignIcon icon-name="diagram-projector" class="w-5 h-5 text-indigo-500" />
- <div>
- <div class="text-lg font-semibold text-gray-900 dark:text-white">
- {{ $t("about.reticulum_stats") }}
- </div>
- <div class="text-xs text-emerald-500 flex items-center gap-1">
- <span class="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span>
- {{ $t("about.live") }}
+ <div v-if="databaseHealth" class="grid grid-cols-2 md:grid-cols-4 gap-8 mb-8">
+ <div
+ class="bg-zinc-50 dark:bg-zinc-950 p-4 rounded-2xl border border-zinc-100 dark:border-zinc-800 shadow-inner"
+ >
+ <div
+ class="text-[9px] font-black text-gray-400 dark:text-zinc-600 uppercase tracking-[0.2em] mb-2 leading-none"
+ >
+ Integrity
+ </div>
+ <div
+ :class="[
+ databaseHealth.quick_check === 'ok' ? 'text-emerald-500' : 'text-red-500',
+ ]"
+ class="text-lg font-black uppercase tracking-tight"
+ >
+ {{ databaseHealth.quick_check }}
+ </div>
</div>
- </div>
- </header>
- <div class="metric-grid">
- <div>
- <div class="glass-label">{{ $t("about.total_paths") }}</div>
- <div class="metric-value">{{ formatNumber(appInfo.reticulum_stats.total_paths) }}</div>
- </div>
- <div>
- <div class="glass-label">{{ $t("about.announces_per_second") }}</div>
- <div class="metric-value">
- {{ formatNumber(appInfo.reticulum_stats.announces_per_second) }}
+ <div
+ class="bg-zinc-50 dark:bg-zinc-950 p-4 rounded-2xl border border-zinc-100 dark:border-zinc-800 shadow-inner"
+ >
+ <div
+ class="text-[9px] font-black text-gray-400 dark:text-zinc-600 uppercase tracking-[0.2em] mb-2 leading-none"
+ >
+ Journal
+ </div>
+ <div class="text-lg font-black uppercase text-blue-500 tracking-tight">
+ {{ databaseHealth.journal_mode }}
+ </div>
</div>
- </div>
- <div>
- <div class="glass-label">{{ $t("about.announces_per_minute") }}</div>
- <div class="metric-value">
- {{ formatNumber(appInfo.reticulum_stats.announces_per_minute) }}
+ <div
+ class="bg-zinc-50 dark:bg-zinc-950 p-4 rounded-2xl border border-zinc-100 dark:border-zinc-800 shadow-inner"
+ >
+ <div
+ class="text-[9px] font-black text-gray-400 dark:text-zinc-600 uppercase tracking-[0.2em] mb-2 leading-none"
+ >
+ Page Count
+ </div>
+ <div class="text-lg font-black font-mono tracking-tight tabular-nums">
+ {{ databaseHealth.page_count }}
+ </div>
</div>
- </div>
- <div>
- <div class="glass-label">{{ $t("about.announces_per_hour") }}</div>
- <div class="metric-value">
- {{ formatNumber(appInfo.reticulum_stats.announces_per_hour) }}
+ <div
+ class="bg-zinc-50 dark:bg-zinc-950 p-4 rounded-2xl border border-zinc-100 dark:border-zinc-800 shadow-inner"
+ >
+ <div
+ class="text-[9px] font-black text-gray-400 dark:text-zinc-600 uppercase tracking-[0.2em] mb-2 leading-none"
+ >
+ Free Space
+ </div>
+ <div class="text-lg font-black text-amber-500 tracking-tight tabular-nums">
+ {{ formatBytes(databaseHealth.estimated_free_bytes) }}
+ </div>
</div>
</div>
- </div>
- </div>
- <div v-if="appInfo?.download_stats" class="glass-card space-y-3">
- <header class="flex items-center gap-2">
- <MaterialDesignIcon icon-name="download" class="w-5 h-5 text-sky-500" />
- <div>
- <div class="text-lg font-semibold text-gray-900 dark:text-white">
- {{ $t("about.download_activity") }}
+ <div class="border-t border-zinc-100 dark:border-zinc-800 pt-8 space-y-8">
+ <!-- Backups -->
+ <div class="flex flex-col md:flex-row md:items-center justify-between gap-6">
+ <div class="space-y-1">
+ <div
+ class="font-black text-gray-900 dark:text-white text-sm tracking-tight flex items-center gap-2"
+ >
+ <v-icon
+ icon="mdi-content-save-all"
+ size="16"
+ class="text-blue-500"
+ ></v-icon>
+ Database Backups
+ </div>
+ <div class="text-xs text-gray-500">
+ Full snapshots of your communications database.
+ </div>
+ </div>
+ <v-btn
+ variant="tonal"
+ color="blue"
+ class="font-black rounded-xl"
+ :loading="backupInProgress"
+ @click="backupDatabase"
+ >
+ <v-icon icon="mdi-download" start></v-icon>
+ Download Backup
+ </v-btn>
</div>
- <div class="text-xs text-emerald-500 flex items-center gap-1">
- <span class="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span>
- {{ $t("about.live") }}
+
+ <!-- Snapshots -->
+ <div class="space-y-6">
+ <div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
+ <div class="space-y-1">
+ <div
+ class="font-black text-gray-900 dark:text-white text-sm tracking-tight flex items-center gap-2"
+ >
+ <v-icon icon="mdi-camera" size="16" class="text-purple-500"></v-icon>
+ Local Snapshots
+ </div>
+ <div class="text-xs text-gray-500">
+ Create point-in-time restore points on disk.
+ </div>
+ </div>
+ <div class="flex gap-2 w-full md:w-auto">
+ <input
+ v-model="snapshotName"
+ type="text"
+ placeholder="Snapshot label..."
+ class="bg-zinc-50 dark:bg-zinc-900 px-4 py-2 rounded-xl text-sm border border-zinc-100 dark:border-zinc-800 focus:outline-none focus:ring-2 focus:ring-blue-500/20 flex-1 md:min-w-[200px]"
+ />
+ <v-btn
+ color="purple"
+ class="font-black rounded-xl"
+ :loading="snapshotInProgress"
+ @click="createSnapshot"
+ >
+ Create
+ </v-btn>
+ </div>
+ </div>
+
+ <div v-if="snapshots.length > 0" class="grid gap-3 sm:grid-cols-2">
+ <div
+ v-for="snapshot in snapshots"
+ :key="snapshot.path"
+ class="flex items-center justify-between p-4 rounded-2xl bg-zinc-50 dark:bg-zinc-900 border border-zinc-100 dark:border-zinc-800 hover:border-purple-500/20 transition-all group"
+ >
+ <div class="flex flex-col">
+ <span
+ class="font-black text-gray-900 dark:text-white text-xs truncate max-w-[150px]"
+ >{{ snapshot.name }}</span
+ >
+ <span class="text-[10px] font-bold text-gray-400 mt-1 tabular-nums"
+ >{{ formatBytes(snapshot.size) }} • {{ snapshot.created_at }}</span
+ >
+ </div>
+ <v-btn
+ variant="tonal"
+ size="x-small"
+ color="purple"
+ class="font-black opacity-0 group-hover:opacity-100 transition-opacity"
+ @click="restoreFromSnapshot(snapshot.path)"
+ >
+ Restore
+ </v-btn>
+ </div>
+ </div>
</div>
- </div>
- </header>
- <div class="metric-value">
- <span v-if="appInfo.download_stats.avg_download_speed_bps !== null">
- {{ formatBytesPerSecond(appInfo.download_stats.avg_download_speed_bps) }}
- </span>
- <span v-else class="text-sm text-gray-500">{{ $t("about.no_downloads_yet") }}</span>
- </div>
- </div>
- </div>
- <div v-if="appInfo" class="glass-card space-y-3">
- <div class="text-lg font-semibold text-gray-900 dark:text-white">
- {{ $t("about.runtime_status") }}
- </div>
- <div class="flex flex-wrap gap-3">
- <span :class="statusPillClass(!appInfo.is_connected_to_shared_instance)">
- <MaterialDesignIcon icon-name="server" class="w-4 h-4" />
- {{
- appInfo.is_connected_to_shared_instance
- ? $t("about.shared_instance")
- : $t("about.standalone_instance")
- }}
- </span>
- <span :class="statusPillClass(appInfo.is_transport_enabled)">
- <MaterialDesignIcon icon-name="transit-connection" class="w-4 h-4" />
- {{
- appInfo.is_transport_enabled
- ? $t("about.transport_enabled")
- : $t("about.transport_disabled")
- }}
- </span>
- </div>
- </div>
+ <!-- Identity Section -->
+ <div class="bg-red-500/5 p-6 rounded-2xl border border-red-500/10 space-y-6">
+ <div class="flex items-center gap-4 text-red-500">
+ <v-icon icon="mdi-key-alert" size="24"></v-icon>
+ <div class="space-y-0.5">
+ <div class="font-black text-sm tracking-tight">Identity Key Control</div>
+ <div
+ class="text-[10px] font-bold uppercase tracking-widest opacity-70 italic"
+ >
+ Critical Security Warning
+ </div>
+ </div>
+ </div>
- <div v-if="config" class="glass-card space-y-4">
- <div class="text-lg font-semibold text-gray-900 dark:text-white">
- {{ $t("about.identity_addresses") }}
- </div>
- <div class="grid gap-3 md:grid-cols-2">
- <div class="address-card">
- <div class="glass-label">{{ $t("app.identity_hash") }}</div>
- <div class="monospace-field break-all">{{ config.identity_hash }}</div>
- <button
- type="button"
- class="secondary-chip mt-3 text-xs"
- @click="copyValue(config.identity_hash, $t('app.identity_hash'))"
- >
- <MaterialDesignIcon icon-name="content-copy" class="w-4 h-4" />
- {{ $t("app.copy") }}
- </button>
- </div>
- <div class="address-card">
- <div class="glass-label">{{ $t("app.lxmf_address") }}</div>
- <div class="monospace-field break-all">{{ config.lxmf_address_hash }}</div>
- <button
- type="button"
- class="secondary-chip mt-3 text-xs"
- @click="copyValue(config.lxmf_address_hash, $t('app.lxmf_address'))"
- >
- <MaterialDesignIcon icon-name="account-network" class="w-4 h-4" />
- {{ $t("app.copy") }}
- </button>
- </div>
- <div class="address-card">
- <div class="glass-label">{{ $t("app.propagation_node") }}</div>
- <div class="monospace-field break-all">
- {{ config.lxmf_local_propagation_node_address_hash || "—" }}
+ <div class="flex flex-wrap gap-3">
+ <v-btn
+ variant="flat"
+ color="red"
+ class="font-black rounded-xl"
+ @click="downloadIdentityFile"
+ >
+ <v-icon icon="mdi-file-export" start></v-icon>
+ Export Key File
+ </v-btn>
+ <v-btn
+ variant="tonal"
+ color="red"
+ class="font-black rounded-xl"
+ @click="copyIdentityBase32"
+ >
+ <v-icon icon="mdi-content-copy" start></v-icon>
+ Copy Base32 Key
+ </v-btn>
+ </div>
+
+ <div class="space-y-4 pt-4 border-t border-red-500/10">
+ <div class="text-[10px] font-black text-red-500/60 uppercase tracking-widest">
+ Restore Identity
+ </div>
+ <div class="flex flex-col sm:flex-row gap-3">
+ <v-btn
+ variant="tonal"
+ color="zinc"
+ class="font-black rounded-xl flex-1 border-dashed border-2"
+ @click="$refs.identityFileInput.click()"
+ >
+ <v-icon icon="mdi-upload" start></v-icon>
+ Upload Key File
+ </v-btn>
+ <input
+ ref="identityFileInput"
+ type="file"
+ accept=".identity,.bin,.key"
+ class="hidden"
+ @change="onIdentityRestoreFileChange"
+ />
+ <div
+ class="text-center sm:py-2 text-[10px] font-black text-zinc-400 uppercase italic px-2 shrink-0 self-center"
+ >
+ — or —
+ </div>
+ <v-btn
+ variant="tonal"
+ color="zinc"
+ class="font-black rounded-xl flex-1 border-dashed border-2"
+ @click="showIdentityPaste = !showIdentityPaste"
+ >
+ <v-icon icon="mdi-clipboard-text" start></v-icon>
+ Paste Base32
+ </v-btn>
+ </div>
+
+ <transition name="fade-blur">
+ <div v-if="showIdentityPaste" class="space-y-3">
+ <textarea
+ v-model="identityRestoreBase32"
+ rows="4"
+ placeholder="Paste your base32 identity key here..."
+ class="w-full bg-white dark:bg-zinc-950 p-4 rounded-xl font-mono text-xs border border-zinc-100 dark:border-zinc-800 focus:outline-none focus:ring-2 focus:ring-red-500/20"
+ ></textarea>
+ <v-btn
+ block
+ color="red"
+ class="font-black rounded-xl"
+ :loading="identityRestoreInProgress"
+ @click="restoreIdentityBase32"
+ >
+ Confirm Key Restore
+ </v-btn>
+ </div>
+ </transition>
+ </div>
+ </div>
</div>
</div>
- <div class="address-card">
- <div class="glass-label">{{ $t("about.telephone_address") }}</div>
- <div class="monospace-field break-all">{{ config.telephone_address_hash || "—" }}</div>
- </div>
</div>
- </div>
+ </transition>
</div>
</div>
</div>
@@ -798,6 +740,8 @@ export default {
electronVersion: null,
chromeVersion: null,
nodeVersion: null,
+ showAdvanced: false,
+ showIdentityPaste: false,
};
},
computed: {
@@ -1003,7 +947,7 @@ export default {
this.restoreInProgress = false;
}
},
- async recoverDatabase() {
+ async runRecovery() {
if (this.databaseActionInProgress) {
return;
}
diff --git a/meshchatx/src/frontend/components/map/MapPage.vue b/meshchatx/src/frontend/components/map/MapPage.vue
index 8348ae6d..bd56dcb9 100644
--- a/meshchatx/src/frontend/components/map/MapPage.vue
+++ b/meshchatx/src/frontend/components/map/MapPage.vue
@@ -5,8 +5,8 @@
class="flex items-center px-4 py-2 border-b border-gray-200 dark:border-zinc-800 bg-white/80 dark:bg-zinc-900/80 backdrop-blur z-10 relative"
>
<div class="flex items-center space-x-2">
- <MaterialDesignIcon icon-name="map" class="size-6 text-blue-500" />
- <h1 class="text-lg font-semibold text-gray-900 dark:text-zinc-100">{{ $t("map.title") }}</h1>
+ <v-icon icon="mdi-map" class="text-blue-500 dark:text-blue-400" size="24"></v-icon>
+ <h1 class="text-xl font-black text-gray-900 dark:text-white">{{ $t("map.title") }}</h1>
</div>
<div class="ml-auto flex items-center space-x-2">
@@ -78,25 +78,25 @@
<!-- drawing toolbar -->
<div class="absolute top-14 left-1/2 -translate-x-1/2 sm:top-2 z-20 flex flex-col gap-2 transform-gpu">
<div
- class="bg-white/90 dark:bg-zinc-900/90 backdrop-blur border border-gray-200 dark:border-zinc-800 rounded-xl shadow-xl overflow-hidden flex flex-row p-1 gap-1"
+ class="bg-white/70 dark:bg-zinc-900/60 backdrop-blur-md rounded-2xl shadow-2xl overflow-hidden flex flex-row p-1 gap-1 border-0"
>
<button
v-for="tool in drawingTools"
:key="tool.type"
- class="p-2 rounded-lg transition-all"
+ class="p-2.5 rounded-xl transition-all hover:scale-110 active:scale-90"
:class="[
- drawType === tool.type && !isMeasuring
+ (drawType === tool.type && !isMeasuring) || (tool.type === 'Export' && isExportMode)
? 'bg-blue-500 text-white shadow-lg shadow-blue-500/30'
: 'hover:bg-gray-100 dark:hover:bg-zinc-800 text-gray-600 dark:text-gray-400',
]"
- :title="$t(`map.tool_${tool.type.toLowerCase()}`)"
- @click="toggleDraw(tool.type)"
+ :title="tool.type === 'Export' ? 'MBTiles exporter' : $t(`map.tool_${tool.type.toLowerCase()}`)"
+ @click="tool.type === 'Export' ? toggleExportMode() : toggleDraw(tool.type)"
>
- <MaterialDesignIcon :icon-name="tool.icon" class="size-6" />
+ <v-icon :icon="'mdi-' + tool.icon" size="22"></v-icon>
</button>
<div class="w-px h-6 bg-gray-200 dark:bg-zinc-800 my-auto mx-1"></div>
<button
- class="p-2 rounded-lg transition-all"
+ class="p-2.5 rounded-xl transition-all hover:scale-110 active:scale-90"
:class="[
isMeasuring
? 'bg-indigo-500 text-white shadow-lg shadow-indigo-500/30'
@@ -105,37 +105,37 @@
:title="$t('map.tool_measure')"
@click="toggleMeasure"
>
- <MaterialDesignIcon icon-name="ruler" class="size-6" />
+ <v-icon icon="mdi-ruler" size="22"></v-icon>
</button>
<button
- class="p-2 rounded-lg hover:bg-red-100 dark:hover:bg-red-900/30 text-red-500 transition-all"
+ class="p-2.5 rounded-xl hover:bg-red-50 dark:hover:bg-red-900/20 text-red-500 transition-all hover:scale-110 active:scale-90"
:title="$t('map.tool_clear')"
@click="clearDrawings"
>
- <MaterialDesignIcon icon-name="trash-can-outline" class="size-6" />
+ <v-icon icon="mdi-trash-can-outline" size="22"></v-icon>
</button>
<div class="w-px h-6 bg-gray-200 dark:bg-zinc-800 my-auto mx-1"></div>
<button
- class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-zinc-800 text-gray-600 dark:text-gray-400 transition-all"
+ class="p-2.5 rounded-xl hover:bg-gray-100 dark:hover:bg-zinc-800 text-gray-600 dark:text-gray-400 transition-all hover:scale-110 active:scale-90"
:title="$t('map.save_drawing')"
@click="showSaveDrawingModal = true"
>
- <MaterialDesignIcon icon-name="content-save-outline" class="size-6" />
+ <v-icon icon="mdi-content-save-outline" size="22"></v-icon>
</button>
<button
- class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-zinc-800 text-gray-600 dark:text-gray-400 transition-all"
+ class="p-2.5 rounded-xl hover:bg-gray-100 dark:hover:bg-zinc-800 text-gray-600 dark:text-gray-400 transition-all hover:scale-110 active:scale-90"
:title="$t('map.load_drawing')"
@click="openLoadDrawingModal"
>
- <MaterialDesignIcon icon-name="folder-open-outline" class="size-6" />
+ <v-icon icon="mdi-folder-open-outline" size="22"></v-icon>
</button>
<div class="w-px h-6 bg-gray-200 dark:bg-zinc-800 my-auto mx-1"></div>
<button
- class="p-2 rounded-lg hover:bg-emerald-100 dark:hover:bg-emerald-900/30 text-emerald-600 dark:text-emerald-400 transition-all"
+ class="p-2.5 rounded-xl hover:bg-blue-50 dark:hover:bg-blue-900/20 text-blue-500 transition-all hover:scale-110 active:scale-90"
:title="$t('map.go_to_my_location')"
@click="goToMyLocation"
>
- <MaterialDesignIcon icon-name="crosshairs-gps" class="size-6" />
+ <v-icon icon="mdi-crosshairs-gps" size="22"></v-icon>
</button>
</div>
</div>
@@ -148,12 +148,12 @@
>
<div class="relative">
<div
- class="flex items-center bg-white/90 dark:bg-zinc-900/90 backdrop-blur rounded-lg shadow-lg border border-gray-200/50 dark:border-zinc-800/50"
+ class="flex items-center bg-white/70 dark:bg-zinc-900/60 backdrop-blur-md rounded-xl shadow-2xl border-0 ring-0"
>
<input
v-model="searchQuery"
type="text"
- class="flex-1 px-3 py-2 bg-transparent text-gray-900 dark:text-zinc-100 placeholder-gray-400 focus:outline-none focus:ring-0 text-sm"
+ class="flex-1 px-4 py-2.5 bg-transparent text-gray-900 dark:text-zinc-100 placeholder-gray-400 focus:outline-none focus:ring-0 border-0 text-sm"
:placeholder="$t('map.search_placeholder')"
@input="onSearchInput"
@keydown.enter="performSearch"
@@ -161,41 +161,45 @@
/>
<button
v-if="searchQuery"
- class="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-zinc-300"
+ class="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-zinc-300 transition-colors"
@click="clearSearch"
>
- <MaterialDesignIcon icon-name="close" class="size-4" />
+ <v-icon icon="mdi-close" size="18"></v-icon>
</button>
<button
- class="p-2 text-blue-500 hover:text-blue-600 disabled:text-gray-300"
+ class="p-2 mr-1 text-blue-500 hover:text-blue-600 disabled:text-gray-300 transition-colors"
:disabled="!searchQuery || isSearching"
@click="performSearch"
>
- <MaterialDesignIcon
- :icon-name="isSearching ? 'loading' : 'magnify'"
- :class="['size-5', { 'animate-spin': isSearching }]"
- />
+ <v-icon
+ :icon="isSearching ? 'mdi-loading' : 'mdi-magnify'"
+ :class="{ 'animate-spin': isSearching }"
+ size="20"
+ ></v-icon>
</button>
</div>
<!-- search results dropdown -->
<div
v-if="isSearchFocused && (searchResults.length > 0 || searchError)"
- class="absolute top-full left-0 right-0 mt-2 bg-white dark:bg-zinc-900 rounded-lg shadow-xl border border-gray-200 dark:border-zinc-800 max-h-64 overflow-y-auto z-40"
+ class="absolute top-full left-0 right-0 mt-2 bg-white dark:bg-zinc-900 rounded-xl shadow-2xl border-0 overflow-y-auto z-40 max-h-64"
>
- <div v-if="searchError" class="p-4 text-sm text-red-500">
+ <div v-if="searchError" class="p-4 text-sm text-red-500 flex items-center gap-2">
+ <v-icon icon="mdi-alert-circle" size="16"></v-icon>
{{ searchError }}
</div>
<button
v-for="(result, index) in searchResults"
:key="index"
- class="w-full px-4 py-3 text-left hover:bg-gray-100 dark:hover:bg-zinc-800 border-b border-gray-100 dark:border-zinc-800 last:border-b-0 transition-colors"
+ class="w-full px-4 py-3 text-left hover:bg-gray-50 dark:hover:bg-zinc-800/50 border-b border-gray-100/50 dark:border-zinc-800/50 last:border-b-0 transition-all"
@click="selectSearchResult(result)"
>
- <div class="font-medium text-gray-900 dark:text-zinc-100 text-sm">
+ <div class="font-bold text-gray-900 dark:text-zinc-100 text-sm">
{{ result.display_name }}
</div>
- <div class="text-xs text-gray-500 dark:text-zinc-400 mt-1">
+ <div
+ class="text-[10px] text-gray-400 dark:text-zinc-500 mt-0.5 font-bold uppercase tracking-wider"
+ >
{{ result.type }}
</div>
</button>
@@ -205,6 +209,63 @@
<div ref="mapContainer" class="absolute inset-0" :class="{ 'cursor-crosshair': isExportMode }"></div>
+ <!-- note hover tooltip -->
+ <div
+ v-if="hoveredNote && !editingFeature"
+ class="absolute pointer-events-none z-50 bg-white/90 dark:bg-zinc-900/90 backdrop-blur border border-gray-200 dark:border-zinc-700 rounded-lg shadow-xl p-2 text-sm text-gray-900 dark:text-zinc-100 max-w-xs transform -translate-x-1/2 -translate-y-full mb-4"
+ :style="{
+ left: map.getPixelFromCoordinate(hoveredNote.getGeometry().getCoordinates())[0] + 'px',
+ top: map.getPixelFromCoordinate(hoveredNote.getGeometry().getCoordinates())[1] + 'px',
+ }"
+ >
+ <div class="font-bold flex items-center gap-1 mb-1 text-amber-500">
+ <MaterialDesignIcon icon-name="note-text" class="size-4" />
+ <span>Note</span>
+ </div>
+ <div class="whitespace-pre-wrap break-words">{{ hoveredNote.get("note") || "Empty note" }}</div>
+ </div>
+
+ <!-- inline note editor (overlay) -->
+ <div ref="noteOverlayElement" class="absolute z-40">
+ <div
+ v-if="editingFeature && !isMobileScreen"
+ class="bg-white dark:bg-zinc-900 rounded-xl shadow-2xl border border-gray-200 dark:border-zinc-700 p-4 w-64 transform -translate-x-1/2 -translate-y-full mb-6"
+ >
+ <div class="flex items-center justify-between mb-2">
+ <span class="text-sm font-bold text-gray-900 dark:text-white flex items-center gap-1">
+ <MaterialDesignIcon icon-name="note-edit" class="size-4 text-amber-500" />
+ Edit Note
+ </span>
+ <button
+ @click="closeNoteEditor"
+ class="text-gray-400 hover:text-gray-600 dark:hover:text-zinc-300"
+ >
+ <MaterialDesignIcon icon-name="close" class="size-4" />
+ </button>
+ </div>
+ <textarea
+ v-model="noteText"
+ class="w-full h-24 p-2 text-sm bg-gray-50 dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded-lg focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none resize-none text-gray-900 dark:text-zinc-100"
+ placeholder="Type your note here..."
+ ></textarea>
+ <div class="flex justify-between mt-3">
+ <button
+ @click="deleteNote"
+ class="px-3 py-1.5 text-xs font-semibold text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors flex items-center gap-1"
+ >
+ <MaterialDesignIcon icon-name="trash-can-outline" class="size-3.5" />
+ Delete
+ </button>
+ <button
+ @click="saveNote"
+ class="px-3 py-1.5 text-xs font-semibold bg-amber-500 text-white hover:bg-amber-600 rounded-lg shadow-sm transition-colors"
+ >
+ Save
+ </button>
+ </div>
+ </div>
+ </div>
+
<!-- loading skeleton for map -->
<div v-if="!isMapLoaded" class="absolute inset-0 z-0 bg-slate-100 dark:bg-zinc-900 animate-pulse">
<div class="grid grid-cols-4 grid-rows-4 h-full w-full gap-1 p-1 opacity-20">
@@ -227,10 +288,10 @@
backgroundColor: selectedMarker.peer?.lxmf_user_icon?.background_colour || '#ffffff',
}"
>
- <MaterialDesignIcon
- :icon-name="selectedMarker.peer?.lxmf_user_icon?.icon_name || 'account'"
- class="size-5"
- />
+ <v-icon
+ :icon="'mdi-' + (selectedMarker.peer?.lxmf_user_icon?.icon_name || 'account')"
+ size="18"
+ ></v-icon>
</div>
<div>
<h3 class="font-bold text-gray-900 dark:text-zinc-100 truncate w-40">
@@ -248,7 +309,7 @@
class="text-gray-500 hover:text-gray-700 dark:hover:text-zinc-300"
@click="selectedMarker = null"
>
- <MaterialDesignIcon icon-name="close" class="size-5" />
+ <v-icon icon="mdi-close" size="20"></v-icon>
</button>
</div>
<div class="p-4 space-y-3">
@@ -294,7 +355,7 @@
</div>
<div class="pt-2 text-[10px] text-gray-400 flex items-center gap-1">
- <MaterialDesignIcon icon-name="clock-outline" class="size-3" />
+ <v-icon icon="mdi-clock-outline" size="12"></v-icon>
Updated: {{ formatTimestamp(selectedMarker.telemetry.timestamp) }}
</div>
@@ -302,7 +363,7 @@
class="w-full py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg font-bold transition-colors text-sm flex items-center justify-center gap-2"
@click="openChat(selectedMarker.telemetry.destination_hash)"
>
- <MaterialDesignIcon icon-name="message-text" class="size-4" />
+ <v-icon icon="mdi-message-text" size="16"></v-icon>
Open Chat
</button>
</div>
@@ -844,6 +905,55 @@
</div>
</div>
</div>
+
+ <!-- mobile note modal -->
+ <transition name="fade">
+ <div
+ v-if="showNoteModal"
+ class="fixed inset-0 z-[100] flex items-end sm:items-center justify-center p-4 bg-black/50 backdrop-blur-sm"
+ @click.self="closeNoteEditor"
+ >
+ <div
+ class="bg-white dark:bg-zinc-900 w-full max-w-lg rounded-t-2xl sm:rounded-2xl shadow-2xl overflow-hidden animate-slide-up sm:animate-fade-in"
+ >
+ <div class="p-4 border-b border-gray-100 dark:border-zinc-800 flex items-center justify-between">
+ <h3 class="text-lg font-bold text-gray-900 dark:text-white flex items-center gap-2">
+ <MaterialDesignIcon icon-name="note-edit" class="size-5 text-amber-500" />
+ Edit Note
+ </h3>
+ <button
+ @click="closeNoteEditor"
+ class="p-2 text-gray-400 hover:bg-gray-100 dark:hover:bg-zinc-800 rounded-full transition-colors"
+ >
+ <MaterialDesignIcon icon-name="close" class="size-5" />
+ </button>
+ </div>
+ <div class="p-4">
+ <textarea
+ v-model="noteText"
+ class="w-full h-40 p-4 text-base bg-gray-50 dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded-xl focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none resize-none text-gray-900 dark:text-zinc-100"
+ placeholder="Type your note here..."
+ autofocus
+ ></textarea>
+ </div>
+ <div class="p-4 bg-gray-50 dark:bg-zinc-800/50 flex justify-between gap-3">
+ <button
+ @click="deleteNote"
+ class="flex-1 px-4 py-3 text-sm font-bold text-red-500 hover:bg-red-100 dark:hover:bg-red-900/20 rounded-xl transition-colors flex items-center justify-center gap-2"
+ >
+ <MaterialDesignIcon icon-name="trash-can-outline" class="size-5" />
+ Delete
+ </button>
+ <button
+ @click="saveNote"
+ class="flex-[2] px-4 py-3 text-sm font-bold bg-amber-500 text-white hover:bg-amber-600 rounded-xl shadow-lg shadow-amber-500/30 transition-colors"
+ >
+ Save Note
+ </button>
+ </div>
+ </div>
+ </div>
+ </transition>
</div>
</template>
@@ -950,9 +1060,11 @@ export default {
isDrawing: false,
drawingTools: [
{ type: "Point", icon: "map-marker-plus" },
+ { type: "Note", icon: "note-text-outline" },
{ type: "LineString", icon: "vector-line" },
{ type: "Polygon", icon: "vector-polygon" },
{ type: "Circle", icon: "circle-outline" },
+ { type: "Export", icon: "crop-free" },
],
// measurement
@@ -965,6 +1077,13 @@ export default {
// drawing storage
savedDrawings: [],
+
+ // note editing
+ editingFeature: null,
+ noteText: "",
+ hoveredNote: null,
+ noteOverlay: null,
+ showNoteModal: false,
showSaveDrawingModal: false,
newDrawingName: "",
isLoadingDrawings: false,
@@ -1219,25 +1338,54 @@ export default {
this.drawSource = new VectorSource();
this.drawLayer = new VectorLayer({
source: this.drawSource,
- style: new Style({
- fill: new Fill({
- color: "rgba(59, 130, 246, 0.2)",
- }),
- stroke: new Stroke({
- color: "#3b82f6",
- width: 3,
- }),
- image: new CircleStyle({
- radius: 7,
+ style: (feature) => {
+ const type = feature.get("type");
+ if (type === "note") {
+ return new Style({
+ image: new CircleStyle({
+ radius: 10,
+ fill: new Fill({
+ color: "#f59e0b",
+ }),
+ stroke: new Stroke({
+ color: "#ffffff",
+ width: 2,
+ }),
+ }),
+ // Use a simple circle for now as custom fonts in canvas can be tricky
+ // or use the built-in Text style if we are sure it works
+ });
+ }
+ return new Style({
fill: new Fill({
+ color: "rgba(59, 130, 246, 0.2)",
+ }),
+ stroke: new Stroke({
color: "#3b82f6",
+ width: 3,
}),
- }),
- }),
+ image: new CircleStyle({
+ radius: 7,
+ fill: new Fill({
+ color: "#3b82f6",
+ }),
+ }),
+ });
+ },
zIndex: 50,
});
this.map.addLayer(this.drawLayer);
+ this.noteOverlay = new Overlay({
+ element: this.$refs.noteOverlayElement,
+ autoPan: {
+ animation: {
+ duration: 250,
+ },
+ },
+ });
+ this.map.addOverlay(this.noteOverlay);
+
this.modify = new Modify({ source: this.drawSource });
this.modify.on("modifyend", () => this.saveMapState());
this.map.addInteraction(this.modify);
@@ -1253,7 +1401,9 @@ export default {
});
this.map.addLayer(this.markerLayer);
+ this.map.on("pointermove", this.handleMapPointerMove);
this.map.on("click", (evt) => {
+ this.handleMapClick(evt);
const feature = this.map.forEachFeatureAtPixel(evt.pixel, (f) => f);
if (feature && feature.get("telemetry")) {
this.onMarkerClick(feature);
@@ -1434,12 +1584,27 @@ export default {
updateMapSource() {
if (!this.map) return;
const layers = this.map.getLayers();
+
+ // Find and replace the tile layer (first layer usually)
+ // or just clear and re-add everything correctly
layers.clear();
+
+ // 1. Tile layer
layers.push(
new TileLayer({
source: this.getTileSource(),
})
);
+
+ // 2. Draw layer
+ if (this.drawLayer) {
+ layers.push(this.drawLayer);
+ }
+
+ // 3. Marker layer
+ if (this.markerLayer) {
+ layers.push(this.markerLayer);
+ }
},
async toggleOffline(enabled) {
if (enabled && !this.hasOfflineMap) {
@@ -1594,8 +1759,9 @@ export default {
this.metadata = response.data.metadata;
this.hasOfflineMap = true;
this.offlineEnabled = true;
+ await this.loadMBTilesList();
+ await this.checkOfflineMap();
this.updateMapSource();
- this.loadMBTilesList();
ToastUtils.success(this.$t("map.upload_success"));
// If the map has bounds, we might want to fit to them
@@ -1895,15 +2061,24 @@ export default {
this.draw = new Draw({
source: this.drawSource,
- type: type,
+ type: type === "Note" ? "Point" : type,
});
this.draw.on("drawstart", () => {
this.isDrawing = true;
});
- this.draw.on("drawend", () => {
+ this.draw.on("drawend", (evt) => {
this.isDrawing = false;
+ const feature = evt.feature;
+ if (type === "Note") {
+ feature.set("type", "note");
+ feature.set("note", "");
+ // Open edit box after a short delay to let the feature settle
+ setTimeout(() => {
+ this.startEditingNote(feature);
+ }, 200);
+ }
// Use setTimeout to ensure the feature is actually in the source before saving
setTimeout(() => this.saveMapState(), 100);
});
@@ -1911,6 +2086,86 @@ export default {
this.map.addInteraction(this.draw);
},
+ startEditingNote(feature) {
+ this.editingFeature = feature;
+ this.noteText = feature.get("note") || "";
+ if (this.isMobileScreen) {
+ this.showNoteModal = true;
+ } else {
+ this.updateNoteOverlay();
+ }
+ },
+
+ updateNoteOverlay() {
+ if (!this.editingFeature || !this.map) return;
+ const geometry = this.editingFeature.getGeometry();
+ const coord = geometry.getCoordinates();
+ this.noteOverlay.setPosition(coord);
+ },
+
+ saveNote() {
+ if (this.editingFeature) {
+ this.editingFeature.set("note", this.noteText);
+ this.saveMapState();
+ }
+ this.closeNoteEditor();
+ },
+
+ cancelNote() {
+ // If it's a new note (no text and just added), we might want to remove it
+ // but for now just close
+ this.closeNoteEditor();
+ },
+
+ closeNoteEditor() {
+ this.editingFeature = null;
+ this.noteText = "";
+ this.showNoteModal = false;
+ if (this.noteOverlay) {
+ this.noteOverlay.setPosition(undefined);
+ }
+ },
+
+ deleteNote() {
+ if (this.editingFeature) {
+ this.drawSource.removeFeature(this.editingFeature);
+ this.saveMapState();
+ }
+ this.closeNoteEditor();
+ },
+
+ handleMapPointerMove(evt) {
+ if (evt.dragging || this.isDrawing || this.isMeasuring) return;
+
+ const pixel = this.map.getEventPixel(evt.originalEvent);
+ const feature = this.map.forEachFeatureAtPixel(pixel, (f) => f, {
+ layerFilter: (l) => l === this.drawLayer,
+ });
+
+ if (feature && feature.get("type") === "note") {
+ this.hoveredNote = feature;
+ this.map.getTargetElement().style.cursor = "pointer";
+ } else {
+ this.hoveredNote = null;
+ this.map.getTargetElement().style.cursor = "";
+ }
+ },
+
+ handleMapClick(evt) {
+ if (this.isDrawing || this.isMeasuring) return;
+
+ const pixel = this.map.getEventPixel(evt.originalEvent);
+ const feature = this.map.forEachFeatureAtPixel(pixel, (f) => f, {
+ layerFilter: (l) => l === this.drawLayer,
+ });
+
+ if (feature && feature.get("type") === "note") {
+ this.startEditingNote(feature);
+ } else {
+ this.closeNoteEditor();
+ }
+ },
+
stopDrawing() {
if (this.draw) {
this.map.removeInteraction(this.draw);
@@ -2351,4 +2606,40 @@ export default {
:deep(.ol-tooltip-static:before) {
border-top-color: #3b82f6;
}
+
+@keyframes slide-up {
+ from {
+ transform: translateY(100%);
+ }
+ to {
+ transform: translateY(0);
+ }
+}
+
+@keyframes fade-in {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+.animate-slide-up {
+ animation: slide-up 0.3s ease-out;
+}
+
+.animate-fade-in {
+ animation: fade-in 0.3s ease-out;
+}
+
+.fade-enter-active,
+.fade-leave-active {
+ transition: opacity 0.3s ease;
+}
+
+.fade-enter-from,
+.fade-leave-to {
+ opacity: 0;
+}
</style>
diff --git a/meshchatx/src/frontend/components/messages/ConversationViewer.vue b/meshchatx/src/frontend/components/messages/ConversationViewer.vue
index 2ed99ec4..d6b94d35 100644
--- a/meshchatx/src/frontend/components/messages/ConversationViewer.vue
+++ b/meshchatx/src/frontend/components/messages/ConversationViewer.vue
@@ -892,12 +892,50 @@
/>
</svg>
</div>
- <h3 class="text-lg font-semibold text-gray-900 dark:text-zinc-100 mb-1">
- {{ $t("messages.no_active_chat") }}
- </h3>
- <p class="text-sm text-gray-500 dark:text-zinc-400">
- {{ $t("messages.select_peer_or_enter_address") }}
- </p>
+ </div>
+ <h3 class="text-lg font-semibold text-gray-900 dark:text-zinc-100 mb-1">
+ {{ $t("messages.no_active_chat") }}
+ </h3>
+ <p class="text-sm text-gray-500 dark:text-zinc-400 mb-8">
+ {{ $t("messages.select_peer_or_enter_address") }}
+ </p>
+
+ <!-- latest chats grid (desktop only) -->
+ <div v-if="!isMobile && latestConversations.length > 0" class="w-full max-w-2xl mb-8">
+ <div class="flex items-center justify-between mb-4">
+ <h4 class="text-xs font-bold text-gray-400 dark:text-zinc-500 uppercase tracking-widest">
+ Latest Chats
+ </h4>
+ </div>
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
+ <div
+ v-for="chat in latestConversations"
+ :key="chat.destination_hash"
+ class="group cursor-pointer p-4 bg-white dark:bg-zinc-900/50 border border-gray-100 dark:border-zinc-800 rounded-2xl hover:border-blue-500/50 hover:shadow-xl hover:shadow-blue-500/5 transition-all duration-300 flex items-center gap-4"
+ @click="$emit('update:selectedPeer', chat)"
+ >
+ <LxmfUserIcon
+ :custom-image="chat.contact_image"
+ :icon-name="chat.lxmf_user_icon ? chat.lxmf_user_icon.icon_name : ''"
+ :icon-foreground-colour="chat.lxmf_user_icon ? chat.lxmf_user_icon.foreground_colour : ''"
+ :icon-background-colour="chat.lxmf_user_icon ? chat.lxmf_user_icon.background_colour : ''"
+ icon-class="size-10"
+ />
+ <div class="flex-1 min-w-0">
+ <div class="font-bold text-gray-900 dark:text-zinc-100 truncate">
+ {{ chat.custom_display_name ?? chat.display_name }}
+ </div>
+ <div class="text-xs text-gray-500 dark:text-zinc-500 truncate mt-0.5">
+ {{ chat.latest_message_preview || chat.latest_message_title || "No messages yet" }}
+ </div>
+ </div>
+ <v-icon
+ icon="mdi-chevron-right"
+ size="18"
+ class="text-gray-300 dark:text-zinc-700 group-hover:text-blue-500 transition-colors"
+ ></v-icon>
+ </div>
+ </div>
</div>
<!-- compose message input -->
@@ -1088,6 +1126,9 @@ export default {
isMobile() {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
},
+ latestConversations() {
+ return this.conversations.slice(0, 4);
+ },
canSendMessage() {
// can send if message text is present
const messageText = this.newMessageText.trim();
diff --git a/meshchatx/src/frontend/components/settings/IdentitiesPage.vue b/meshchatx/src/frontend/components/settings/IdentitiesPage.vue
index ecb7af34..7165ba35 100644
--- a/meshchatx/src/frontend/components/settings/IdentitiesPage.vue
+++ b/meshchatx/src/frontend/components/settings/IdentitiesPage.vue
@@ -91,8 +91,23 @@
</div>
<div
class="text-xs font-mono text-gray-500 dark:text-zinc-500 truncate mt-0.5 tracking-tight"
+ :title="'RNS: ' + identity.hash"
>
- {{ identity.hash }}
+ ID: {{ identity.hash }}
+ </div>
+ <div
+ v-if="identity.lxmf_address"
+ class="text-[10px] font-mono text-gray-400 dark:text-zinc-600 truncate mt-0.5 tracking-tighter"
+ :title="'LXMF: ' + identity.lxmf_address"
+ >
+ LXMF: {{ identity.lxmf_address }}
+ </div>
+ <div
+ v-if="identity.lxst_address"
+ class="text-[10px] font-mono text-gray-400 dark:text-zinc-600 truncate mt-0.5 tracking-tighter"
+ :title="'LXST: ' + identity.lxst_address"
+ >
+ LXST: {{ identity.lxst_address }}
</div>
</div>
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────